package cn.qianxun.meta.auth.login.service.impl;

import cn.dev33.satoken.jwt.SaJwtUtil;
import cn.dev33.satoken.secure.BCrypt;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import cn.qianxun.meta.auth.login.properties.UserPasswordProperties;
import cn.qianxun.meta.auth.thirdlogin.dto.ThirdLoginVO;
import cn.qianxun.meta.common.log.event.LogininforEvent;
import com.anji.captcha.model.common.ResponseModel;
import com.anji.captcha.model.vo.CaptchaVO;
import com.anji.captcha.service.CaptchaService;
import cn.qianxun.meta.auth.login.dto.ApplicationLoginDTO;
import cn.qianxun.meta.auth.login.dto.LoginInfoDTO;
import cn.qianxun.meta.auth.login.service.ISysLoginService;
import cn.qianxun.meta.auth.login.vo.ApiTokenVO;
import cn.qianxun.meta.auth.login.vo.TokenVO;
import cn.qianxun.meta.auth.thirdlogin.util.AuthTokenUtil;
import cn.qianxun.meta.auth.thirdlogin.util.ThirdLoginEnum;
import cn.qianxun.meta.common.core.constant.CacheConstants;
import cn.qianxun.meta.common.core.constant.Constants;
import cn.qianxun.meta.common.core.constant.HttpStatus;
import cn.qianxun.meta.common.core.domain.LoginUser;
import cn.qianxun.meta.common.core.domain.Result;
import cn.qianxun.meta.common.core.enums.DeviceType;
import cn.qianxun.meta.common.core.enums.LoginType;
import cn.qianxun.meta.common.core.enums.UserType;
import cn.qianxun.meta.common.core.exception.CaptchaException;
import cn.qianxun.meta.common.core.exception.ServiceException;
import cn.qianxun.meta.common.core.exception.user.CaptchaExpireException;
import cn.qianxun.meta.common.core.utils.*;
import cn.qianxun.meta.common.core.utils.ip.AddressUtils;
import cn.qianxun.meta.common.satoken.utils.LoginHelper;
import cn.qianxun.meta.common.satoken.utils.StpAppIdUtil;
import cn.qianxun.meta.config.api.feign.ISysConfigProvider;
import cn.qianxun.meta.redis.utils.RedisUtils;
import cn.qianxun.meta.user.api.feign.ISysUserProvider;
import cn.qianxun.meta.user.api.vo.ApiAuthApplicationVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.Supplier;

/**
 * Copyright:版权所有(c)qianxun
 *
 * @Author fuzhilin
 * @Description //TODO
 * @Date 2023/8/21 17:01
 **/
@Slf4j
@Service
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class SysLoginServiceImpl implements ISysLoginService {

    private final ISysUserProvider sysUserProvider;
    private final ISysConfigProvider sysConfigProvider;

    private final UserPasswordProperties userPasswordProperties;

    private final CaptchaService captchaService;

    @Value("${spring.profiles.active}")
    private String profile;

    public TokenVO login(LoginInfoDTO dto) {

        CaptchaVO captchaVO = new CaptchaVO();
        captchaVO.setCaptchaVerification(dto.getCaptchaVerification());
        captchaVO.setCaptchaType(dto.getCaptchaType());
        captchaVO.setPointJson(dto.getPointJson());
        captchaVO.setToken(dto.getToken());
        if (!Constants.DEV.equals(profile)) {
            ResponseModel response = captchaService.verification(captchaVO);
            if (response.isSuccess() == false) {
                String recode = response.getRepCode();
                if ("6110".equals(recode)) {
                    throw new CaptchaExpireException();
                } else if ("0000".equals(recode)) {

                } else {
                    throw new CaptchaException();
                }
            }
        }
        LoginUser userInfo = sysUserProvider.getUserInfo(dto.getUsername());
        TokenVO tokenVO = new TokenVO();
        checkLogin(LoginType.PASSWORD, dto.getUsername(), () -> !BCrypt.checkpw(dto.getPassword(), userInfo.getPassword()));
        LoginHelper.loginByDevice(userInfo, DeviceType.PC);
        tokenVO.setToken(StpUtil.getTokenValue());
        recordLogininfor(dto.getUsername(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
        return tokenVO;
    }

    @Override
    public ApiTokenVO applicationLogin(ApplicationLoginDTO dto) {
        Result<ApiAuthApplicationVO> result = sysUserProvider.getAppInfoByAppId(dto.getAppId(), dto.getAppSecret());
        if (HttpStatus.SUCCESS != result.getCode()) {
            throw new ServiceException(result.getMsg());
        }
        checkLogin(LoginType.APPID, dto.getAppId(), () -> false);
        ApiAuthApplicationVO applicationVO = result.getData();
        //判断登录IP是否在白名单内 - 未配置IP白名单则不拦截
        if (StringUtils.isNotEmpty(applicationVO.getIpWhites())) {
            final String ip = ServletUtils.getClientIP(ServletUtils.getRequest());
            if (!ArrayUtil.contains(applicationVO.getIpWhites().split(","), ip)) {
                //IP白名单中不包含登录IP，记录日志，并抛出提示
                recordLogininfor(Constants.APPID + dto.getAppId(), Constants.LOGIN_FAIL, "登录IP未授权");
                throw new ServiceException("登录IP未授权");
            }
        }
        //判断appId的访问时间是否到期 - 未配置到期时间则不拦截
        Date accessLimitTime = applicationVO.getAccessTimeLimit();
        if (accessLimitTime != null) {
            Date now = new Date();
            Date dateNow = DateUtils.formatStringToDate(DateUtils.formatDateToString(now, "yyyy-MM-dd HH:mm:ss"), "yyyy-MM-dd");
            boolean isExpire = dateNow.before(accessLimitTime);
            if (!isExpire) {
                throw new ServiceException("应用的访问期限已到期");
            }
        }
        //判断限制次数
        Integer limitLoginNum = applicationVO.getLimitLoginNum();
        if (StringUtils.isNotNull(limitLoginNum) && -1 != limitLoginNum) {
            Integer alreadyLoginNum = applicationVO.getAlreadyLoginNum();
            if (StringUtils.isNotNull(alreadyLoginNum)) {
                if (alreadyLoginNum > limitLoginNum) {
                    throw new ServiceException("应用的访问次数已达上限");
                } else {
                    //更新登录次数 只有真正过期才会计算
                    if (!StpAppIdUtil.isLogin(Constants.APPID + dto.getAppId())) {
                        sysUserProvider.updateApplicationLoginNum(applicationVO.getId());
                    }
                }
            }
        }
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(applicationVO.getAppId());
        loginUser.setUserName(String.valueOf(applicationVO.getAppId()));
        loginUser.setDeptName(applicationVO.getName());
        loginUser.setUserType(UserType.APP_ID.getUserType());
        //获取appId拥有的权限
        loginUser.setAppIdPermission(applicationVO.getMenuPermission());
        //登录
        ApiTokenVO tokenVO = new ApiTokenVO();
        LoginHelper.appIdLoginByDevice(loginUser, DeviceType.APPID);
        tokenVO = createAppAccessToken(String.valueOf(applicationVO.getAppId()), dto.getAppSecret());
        recordLogininfor(Constants.APPID + dto.getAppId(), Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"));
        return tokenVO;
    }

    @Override
    public ApiTokenVO getApplicationToken(String refreshToken) {
        if (StpAppIdUtil.isLogin()) {
            throw new ServiceException("accessToken未过期，无法重新获取令牌信息");
        }
        ApplicationLoginDTO loginDTO = RedisUtils.getCacheObject(Constants.APPID_REFRESH_TOKEN + refreshToken);
        if (StringUtils.isNull(loginDTO)) {
            throw new ServiceException("refreshToken已过期，无法访问系统资源，请重新登录认证");
        }
        ApiTokenVO tokenVO = applicationLogin(loginDTO);
        //删除之前的refreshToken
        RedisUtils.deleteObject(Constants.APPID_REFRESH_TOKEN + refreshToken);
        return tokenVO;
    }

    @Override
    public TokenVO thirdLogin(ThirdLoginVO vo) {
        //判断登录IP是否在白名单内
        String loginIp = ServletUtils.getClientIP(ServletUtils.getRequest());
        log.info("登录的ip:{}", loginIp);
        if (!NetUtil.isInnerIP(loginIp)) {
            String puYangThirdLoginIp = sysConfigProvider.getConfigKey("puYangThirdLoginIp").getData();
            if (StringUtils.isEmpty(puYangThirdLoginIp)) {
                throw new ServiceException("未配置登录IP白名单");
            }
            log.info("配置的登录IP白名单为：{}", puYangThirdLoginIp);
            List<String> ips = Arrays.asList(puYangThirdLoginIp.split(","));
            if (!ips.contains("0.0.0.0")) {
                if (!ips.contains(loginIp)) {
                    throw new ServiceException("登录IP未授权");
                }
            }
        }
        String account = vo.getAccount();
        String timestamp = vo.getTimestamp();
        long oldTimestamp = 0;
        try {
            oldTimestamp = Long.valueOf(timestamp);
        } catch (NumberFormatException e) {
            throw new ServiceException("时间戳格式不正确");
        }
        long nowTimestamp = System.currentTimeMillis();
        long diff = nowTimestamp - oldTimestamp;
        log.info("超时前时间差为：" + diff);
        if (diff > 10000) {
            log.info("超过10秒，超时后时间差为：" + diff);
            recordLogininfor(account, Constants.LOGIN_FAIL, "濮阳第三方登录失败-登录失败token已失效");
            throw new ServiceException("登录失败token已失效");
        }
        String authToken = vo.getToken();
        String code = ThirdLoginEnum.CODE.getCode();
        String accessKey = ThirdLoginEnum.ACCESS_KEY.getCode();
        //对账号进行解密
        try {
            account = AuthTokenUtil.decryptContent(account, accessKey);
        } catch (Exception e) {
            e.printStackTrace();
            recordLogininfor(account, Constants.LOGIN_FAIL, "濮阳第三方登录失败-账号解析失败");
            throw new ServiceException("账号解析失败");
        }
        log.info("解析账号为:{}", account);
        String socAuthToken = null;
        try {
            socAuthToken = AuthTokenUtil.generateAuthToken(accessKey, Long.valueOf(timestamp), code, account);
        } catch (NumberFormatException e) {
            e.printStackTrace();
            recordLogininfor(account, Constants.LOGIN_FAIL, "濮阳第三方登录失败-authToken解析失败");
            throw new ServiceException("authToken解析失败");
        }
        if (!authToken.equals(socAuthToken)) {
            recordLogininfor(account, Constants.LOGIN_FAIL, "濮阳第三方登录失败-用户名或密码不正确");
            throw new ServiceException("用户名或密码不正确");
        }
        //进行登录操作
        LoginUser userInfo = sysUserProvider.getUserInfo(account);
        TokenVO tokenVO = new TokenVO();
        LoginHelper.loginByDevice(userInfo, DeviceType.PC);
        tokenVO.setToken(StpUtil.getTokenValue());
        recordLogininfor(account, Constants.LOGIN_SUCCESS, "濮阳第三方登录成功");
        return tokenVO;
    }

    /**
     * 创建app应用accessToken
     *
     * @param appId     appid
     * @param appSecret appSecret
     * @return ApiTokenVO
     */
    private ApiTokenVO createAppAccessToken(String appId, String appSecret) {
        ApiTokenVO tokenVO = new ApiTokenVO();
        ApplicationLoginDTO loginDTO = new ApplicationLoginDTO();
        loginDTO.setAppId(appId);
        loginDTO.setAppSecret(appSecret);
        //获取旧的刷新token
        String oldRefreshToken = RedisUtils.getCacheObject(Constants.APPID_REFRESH_TOKEN + appId);
        if (StringUtils.isNotEmpty(oldRefreshToken)) {
            RedisUtils.deleteObject(Constants.APPID_REFRESH_TOKEN + oldRefreshToken);
            RedisUtils.deleteObject(Constants.APPID_REFRESH_TOKEN + appId);
        }
        //产生新的刷新token
        //创建刷新token 刷新token的有效期是accessToken的2倍
        String newRefreshToken = SaJwtUtil.createToken(DeviceType.APPID.getDevice(), appId, null, "yun_zhi_xin_an_meta");
        RedisUtils.setCacheObject(Constants.APPID_REFRESH_TOKEN + newRefreshToken, loginDTO, Duration.ofSeconds(StpAppIdUtil.getTokenTimeout() * 2));
        RedisUtils.setCacheObject(Constants.APPID_REFRESH_TOKEN + appId, newRefreshToken, Duration.ofSeconds(StpAppIdUtil.getTokenTimeout() * 2));
        tokenVO.setAccessToken(StpAppIdUtil.getTokenValue());
        tokenVO.setRefreshToken(newRefreshToken);
        tokenVO.setExpireTime(StpAppIdUtil.getTokenTimeout() * 2);
        return tokenVO;
    }


    /**
     * 记录登录信息
     *
     * @param username 用户名
     * @param status   状态
     * @param message  消息内容
     * @return
     */
    public void recordLogininfor(String username, String status, String message) {
        HttpServletRequest request = ServletUtils.getRequest();
        final UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
        final String ip = ServletUtils.getClientIP(request);

        String address = AddressUtils.getRealAddressByIP(ip);
        // 获取客户端操作系统
        String os = userAgent.getOs().getName();
        // 获取客户端浏览器
        String browser = userAgent.getBrowser().getName();
        // 封装对象
        LogininforEvent logininfor = new LogininforEvent();
        logininfor.setUserName(username);
        logininfor.setIpaddr(ip);
        logininfor.setLoginLocation(address);
        logininfor.setBrowser(browser);
        logininfor.setOs(os);
        logininfor.setMsg(message);
        // 日志状态
        if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT)) {
            logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS);
        } else if (Constants.LOGIN_FAIL.equals(status)) {
            logininfor.setStatus(Constants.LOGIN_FAIL_STATUS);
        }
        SpringUtils.context().publishEvent(logininfor);
    }


    /**
     * 登录校验
     */
    private void checkLogin(LoginType loginType, String username, Supplier<Boolean> supplier) {
        String errorKey = CacheConstants.PWD_ERR_CNT_KEY + username;
        String loginFail = Constants.LOGIN_FAIL;
        Integer maxRetryCount = userPasswordProperties.getMaxRetryCount();
        Integer lockTime = userPasswordProperties.getLockTime();
        // 获取用户登录错误次数(可自定义限制策略 例如: key + username + ip)
        Integer errorNumber = RedisUtils.getCacheObject(errorKey);
        // 锁定时间内登录 则踢出
        if (ObjectUtil.isNotNull(errorNumber) && errorNumber.equals(maxRetryCount)) {
            recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
            throw new ServiceException("账号或密码错误次数过多，请" + lockTime + "分钟后再试");
        }
        if (supplier.get()) {
            // 是否第一次
            errorNumber = ObjectUtil.isNull(errorNumber) ? 1 : errorNumber + 1;
            // 达到规定错误次数 则锁定登录
            if (errorNumber.equals(maxRetryCount)) {
                RedisUtils.setCacheObject(errorKey, errorNumber, Duration.ofMinutes(lockTime));
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitExceed(), maxRetryCount, lockTime));
                throw new ServiceException("输入账号或密码错误次数过多，已被锁定");
            } else {
                // 未达到规定错误次数 则递增
                RedisUtils.setCacheObject(errorKey, errorNumber);
                recordLogininfor(username, loginFail, MessageUtils.message(loginType.getRetryLimitCount(), errorNumber));
                throw new ServiceException("账号或密码错误");
            }
        }
        // 登录成功 清空错误次数
        RedisUtils.deleteObject(errorKey);
    }
}
